{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {
    "colab_type": "text",
    "id": "view-in-github"
   },
   "source": [
    "<a href=\"https://colab.research.google.com/github/tomasonjo/blogs/blob/master/gds_python/gds_python_intro.ipynb\" target=\"_parent\"><img src=\"https://colab.research.google.com/assets/colab-badge.svg\" alt=\"Open In Colab\"/></a>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "kHob0RF8YDrx"
   },
   "source": [
    "# How to get started with the Neo4j Graph Data Science Python client\n",
    "## Learn the basic syntax of the newly released Python client for Neo4j Graph Data Science library\n",
    "\n",
    "Data scientists like me love Python. It features a wide variety of machine learning and data science libraries that can help you get started on a data science project in minutes. It is not uncommon to use a variety of libraries in a data science workflow. With the release of version 2 of the Neo4j Graph Data Science (GDS) library, a supporting Python client has been introduced. The Python client for the GDS library is designed to help you seamlessly integrate the Neo4j Graph Data Science library into your data science workflow. Instead of having to write Cypher statements to execute graph algorithms, the Python client provides a simple surface that allows you to project and run graph algorithms using pure Python code.\n",
    "\n",
    "Since the Python client for GDS is relatively new, there are not many examples out there yet. Therefore, I've decided to write this blog post to help you get started with the GDS Python client syntax and show some common usage patterns through a simple network analysis.\n",
    "\n",
    "The Neo4j Graph Data Science Python client can be installed using the pip package installer."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/"
    },
    "id": "Q1KlTgmR8PAL",
    "outputId": "db6489bd-cf34-4f97-8b44-77f08b3e9b73"
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Looking in indexes: https://pypi.org/simple, https://us-python.pkg.dev/colab-wheels/public/simple/\n",
      "Requirement already satisfied: graphdatascience in /usr/local/lib/python3.7/dist-packages (1.0.0)\n",
      "Requirement already satisfied: pandas<2.0,>=1.0 in /usr/local/lib/python3.7/dist-packages (from graphdatascience) (1.3.5)\n",
      "Requirement already satisfied: neo4j<5.0,>=4.4.2 in /usr/local/lib/python3.7/dist-packages (from graphdatascience) (4.4.3)\n",
      "Requirement already satisfied: pytz in /usr/local/lib/python3.7/dist-packages (from neo4j<5.0,>=4.4.2->graphdatascience) (2022.1)\n",
      "Requirement already satisfied: numpy>=1.17.3 in /usr/local/lib/python3.7/dist-packages (from pandas<2.0,>=1.0->graphdatascience) (1.21.6)\n",
      "Requirement already satisfied: python-dateutil>=2.7.3 in /usr/local/lib/python3.7/dist-packages (from pandas<2.0,>=1.0->graphdatascience) (2.8.2)\n",
      "Requirement already satisfied: six>=1.5 in /usr/local/lib/python3.7/dist-packages (from python-dateutil>=2.7.3->pandas<2.0,>=1.0->graphdatascience) (1.15.0)\n"
     ]
    }
   ],
   "source": [
    "!pip install graphdatascience"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "tv2vtJOQYLQW"
   },
   "source": [
    "An important thing to note is that the Python client is only guaranteed to work with GDS versions 2.0 and later. Therefore, if you have a previous version, I suggest you first upgrade the GDS library to the latest version.\n",
    "# Neo4j environment setup\n",
    "If you want to follow along with the code examples, you need to set up a Neo4j database. I suggest you use a [blank project on Neo4j Sandbox](https://sandbox.neo4j.com/?usecase=blank-sandbox) for this simple demonstration, but you can also download a [Neo4j Desktop application](https://neo4j.com/download/) and set up a local database.\n",
    "\n",
    "Neo4j Sandbox has the GDS library already installed. However, if you use Neo4j Desktop, you have to install the GDS library manually.\n",
    "\n",
    "# Setting up the GDS Python client connection\n",
    "We start by defining the client connection to the Neo4j database. If you have seen any of my previous blog posts that use the official Neo4j Python driver, you can see that the syntax is almost identical."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {
    "id": "GKTUobMl8S31"
   },
   "outputs": [],
   "source": [
    "from graphdatascience import GraphDataScience\n",
    "\n",
    "host = \"bolt://44.193.28.203:7687\"\n",
    "user = \"neo4j\"\n",
    "password= \"combatants-coordinates-tugs\"\n",
    "\n",
    "gds = GraphDataScience(host, auth=(user, password))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "FWOhP1TdYehq"
   },
   "source": [
    "We have instantiated the connection to the Neo4j instance. If you are using Neo4j Enterprise, you might have multiple databases available in Neo4j. If we want to use any database other than the default one, we can select the required database using the set_database  method."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {
    "id": "lk93jlvJ-u6L"
   },
   "outputs": [],
   "source": [
    "# Optionally set different database\n",
    "#gds.set_database(\"databaseName\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "rZbDBeZnYhEJ"
   },
   "source": [
    "Lastly, we can verify that the connection is valid and the target Neo4j instance has the GDS library installed by using the `gds.version()` method."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/"
    },
    "id": "-Ah7nqZD6Zcd",
    "outputId": "42ae6efd-561d-45e5-d601-27d5e562c580"
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "2.3.0\n"
     ]
    }
   ],
   "source": [
    "# Check if connection is valid and the target database has GDS installed\n",
    "print(gds.version())"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "Ad85KnNeYlwa"
   },
   "source": [
    "The version() method should return the version of the installed GDS library. If it returns anything else, make sure that you entered the correct credentials and the GDS library is installed.\n",
    "# Executing Cypher statements\n",
    "The Python client allows you to execute arbitrary Cypher statements using the `run_cypher` method. The method takes two parameters are input. The first and mandatory parameter is the Cypher query you want to execute. The second method parameter is optional and can be used to provide any query parameters.\n",
    "\n",
    "The `run_cypher` method can be used to import, transform, or fetch any data from the database. We will begin by populating the database with the [Harry Potter network](https://medium.com/neo4j/turn-a-harry-potter-book-into-a-knowledge-graph-ffc1c45afcc8) I created in one of my previous blog posts.\n",
    "\n",
    "The network contains characters in the first book, and their interactions, which are represented as relationships. The CSV with the relationship is available on my GitHub, so we can use the `LOAD CSV` clause to retrieve the data from GitHub and store it into Neo4j."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/",
     "height": 49
    },
    "id": "GaXfepPu_YZk",
    "outputId": "390e4c6d-d5b3-4191-e1ba-0437f8752363"
   },
   "outputs": [
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "Empty DataFrame\n",
       "Columns: []\n",
       "Index: []"
      ]
     },
     "execution_count": 4,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "query = \"\"\"\n",
    "LOAD CSV WITH HEADERS FROM $url AS row\n",
    "MERGE (s:Character {name:row.source})\n",
    "MERGE (t:Character {name:row.target})\n",
    "MERGE (s)-[i:INTERACTS]->(t)\n",
    "SET i.weight = toInteger(row.weight)\n",
    "\"\"\"\n",
    "params = {'url': 'https://raw.githubusercontent.com/tomasonjo/blog-datasets/main/HP/hp_1.csv'}\n",
    "gds.run_cypher(query, params)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "egAEqSvfY6lz"
   },
   "source": [
    "The import script uses the `run_cypher` method to execute the Cypher statement used to import the Harry Potter network. To demonstrate how Cypher parameters work with the `run_cypher` method, I've attached the URL of the file as a Cypher parameter. While the Cypher query is represented as a string, the Cypher parameters are defined as a dictionary.\n",
    "If you have done any data analysis in Python, you have probably used the Pandas library in your workflow. Therefore, when fetching data from a database using the run_cyphermethod, the method conveniently returns a populated Pandas DataFrame. Having the data available as a Pandas DataFrame makes it much easier to integrate the data from Neo4j into your analytical workflow and use it in combination with other libraries.\n",
    "\n",
    "In this example, we will retrieve the degree (count of relationships) for each character in the network using the `run_cypher` method."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/",
     "height": 206
    },
    "id": "a50_Aq0GAiHR",
    "outputId": "fb6dabb6-edda-4ae4-d924-65618ae9c5f3"
   },
   "outputs": [
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>character</th>\n",
       "      <th>degree</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>Petunia Dursley</td>\n",
       "      <td>8</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>Dudley Dursley</td>\n",
       "      <td>14</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>Lily J. Potter</td>\n",
       "      <td>5</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>James Potter I</td>\n",
       "      <td>5</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>Harry Potter</td>\n",
       "      <td>83</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "         character  degree\n",
       "0  Petunia Dursley       8\n",
       "1   Dudley Dursley      14\n",
       "2   Lily J. Potter       5\n",
       "3   James Potter I       5\n",
       "4     Harry Potter      83"
      ]
     },
     "execution_count": 5,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "degree_df = gds.run_cypher(\"\"\"\n",
    "MATCH (c:Character)\n",
    "RETURN c.name AS character,\n",
    "       count{ (c)--() } AS degree\n",
    "\"\"\")\n",
    "\n",
    "degree_df.head()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "mEsbwfP3ZGdj"
   },
   "source": [
    "Since the data is available as a Pandas DataFrame, we can easily integrate it into our analytical workflow. For example, we can use the Seaborn library to visualize the node degree distribution."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/",
     "height": 530
    },
    "id": "DhCt2ueTA4SA",
    "outputId": "27ba275a-76c5-4a07-b798-46e0f0e261c5"
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "<seaborn.axisgrid.FacetGrid at 0x7f1afafda610>"
      ]
     },
     "execution_count": 6,
     "metadata": {},
     "output_type": "execute_result"
    },
    {
     "data": {
      "image/png": "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\n",
      "text/plain": [
       "<Figure size 756x504 with 1 Axes>"
      ]
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    }
   ],
   "source": [
    "import seaborn as sns\n",
    "import matplotlib.pyplot as plt\n",
    "\n",
    "sns.displot(data=degree_df, x=\"degree\", height=7, aspect=1.5)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "fkER78KrZOxT"
   },
   "source": [
    "We can easily observe that most nodes have less than 15 relationships. However, there is one outlier in the dataset with 83 connections, and that is, of course, Harry Potter himself.\n",
    "# Projected graph object\n",
    "The central concept of the GDS Python client is to allow projecting and executing graph algorithms in Neo4j with pure Python code. Furthermore, the Python client is designed to mimic the GDS Cypher procedures so that we don't have to learn a new syntax to use the Python client.\n",
    "\n",
    "As you might know, before we can execute any graph algorithms, we first have to project an in-memory graph. For example, let's say we want to project a simple directed network of characters and their interactions with the Python client.\n",
    "\n",
    "![mapping.png]()\n",
    "\n",
    "If you are familiar with Cypher procedures of the Graph Data Science library, you will be able to pick up the Python client syntax easily. For the most part, we remove the CALLclause before the GDS procedures, and we get the Python client syntax to project graphs or execute algorithms.\n",
    "\n",
    "In our case, we want to project a network of characters where the interaction relationships are treated as undirected. Therefore, we must use the extended map syntax to define undirected relationships.\n",
    "\n",
    "![Copy of mapping_graph.drawio (1).png]()\n",
    "\n",
    "When dealing with map objects, or dictionaries as they are called in Python, we have to add quotes around map keys. Otherwise, the keys would be treated as variables in Python, and you would get a NameError as the key variables are not defined. So, apart from adding quotes and removing the CALLclause, the syntax to project an in-memory graph is identical. \n",
    "\n",
    "When projecting a graph with the Python client, a client-side reference to the projected graph is returned. We call these references Graph objects. Along with the Graph object, the metadata from the procedure call is returned as Pandas Series.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {
    "id": "c78q70AkB1ZQ"
   },
   "outputs": [
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "36cbe152ce7d42dea0d178c4d05d76ad",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "Loading:   0%|          | 0/100 [00:00<?, ?%/s]"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "G, metadata = gds.graph.project(\n",
    "    \"hp-graph\", \n",
    "    \"Character\",\n",
    "    {\"INTERACTS\": {\"orientation\": \"UNDIRECTED\", \"properties\": [\"weight\"]}}\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "t0M0fvVZZ3KO"
   },
   "source": [
    "We have passed the projected graph reference to the `G` variable and stored the metadata information as the `metadata` variable. The metadata variable contains information that you normally get as the output of the procedure."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/"
    },
    "id": "vIfLiaKYDm6J",
    "outputId": "70824dd4-0281-40d4-ae20-e46cbb1f8947"
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "nodeProjection            {'Character': {'label': 'Character', 'properti...\n",
       "relationshipProjection    {'INTERACTS': {'orientation': 'UNDIRECTED', 'i...\n",
       "graphName                                                          hp-graph\n",
       "nodeCount                                                               119\n",
       "relationshipCount                                                       812\n",
       "projectMillis                                                          1628\n",
       "Name: 0, dtype: object"
      ]
     },
     "execution_count": 8,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "metadata"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "VbJEhSOXZ9ze"
   },
   "source": [
    "There are 119 nodes and 812 relationships in our projected graph.\n",
    "The Graph object, available as the variable G , has multiple method that can be used to inspect more information about the projected graph. For a complete list of the methods consult with the [official documentation](https://neo4j.com/docs/graph-data-science/current/python-client/graph-object/#_inspecting_a_graph_object).\n",
    "\n",
    "For example, we can return the projected graph name using the `name()` method, inspect the memory usage using the `memory_usage()` method, or even calculate the density of the graph using the `density()` method."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/"
    },
    "id": "xll1y1m9DsH5",
    "outputId": "1351780f-5673-4195-f10c-fd79a2b5338e"
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "hp-graph\n",
      "2342 KiB\n",
      "0.05782652043868395\n"
     ]
    }
   ],
   "source": [
    "print(G.name())\n",
    "print(G.memory_usage())\n",
    "print(G.density())"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "so0n--2RaJzn"
   },
   "source": [
    "# Running graph algorithms\n",
    "Now that we have the projected graph ready and available as the reference variable `G` , we can go ahead and execute a couple of graph algorithms using the Python client.\n",
    "\n",
    "We will begin by executing the weighted variant of the PageRank algorithm. The `stream` mode of the algorithm returns the result of the algorithm as a stream of records.\n",
    "\n",
    "![gds_pagerank.drawio (1).png]()\n",
    "\n",
    "Similar to before, when we were projecting an in-memory graph, we need to remove the `CALL` clause in the Python client for all algorithm executions. We reference the projected graph by its name with the Cypher procedure statement. However, using the Python client, we pass the Graph object as the reference to the projected in-memory graph instead of its name. Lastly, any algorithm configuration parameters can be specified as keyword arguments in the Python client.\n",
    "We can use the following Python script to execute the `stream` mode of the weighted PageRank algorithm."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/",
     "height": 206
    },
    "id": "a0pLRI1XV5gi",
    "outputId": "942c4d98-a44a-4875-f488-04877744d15a"
   },
   "outputs": [
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>nodeId</th>\n",
       "      <th>score</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>0</td>\n",
       "      <td>1.851142</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>1</td>\n",
       "      <td>3.241780</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>2</td>\n",
       "      <td>0.375610</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>3</td>\n",
       "      <td>0.375610</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>4</td>\n",
       "      <td>24.197442</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "   nodeId      score\n",
       "0       0   1.851142\n",
       "1       1   3.241780\n",
       "2       2   0.375610\n",
       "3       3   0.375610\n",
       "4       4  24.197442"
      ]
     },
     "execution_count": 10,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# PageRank stream\n",
    "pagerank_df = gds.pageRank.stream(G, relationshipWeightProperty=\"weight\")\n",
    "pagerank_df.head()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "EuKnJ7Yzacb-"
   },
   "source": [
    "The `stream` mode of any algorithm in the GDS library returns a stream of records. Python client then automatically converts the output into a Pandas DataFrame.\n",
    "\n",
    "If you have ever executed the `stream` mode of the graph algorithms in Neo4j GDS library, you might be aware that the result contains internal node ids as a reference to nodes instead of actual node objects. The `pagerank_df` DataFrame contains two columns:\n",
    "* nodeId: Internal node ids used to reference nodes\n",
    "* score: PageRank score\n",
    "\n",
    "We can retrieve the referenced node objects using the `nodeId` column without constructing a Cypher statement by using the `gds.util.asNodes()` method. The `gds.util.asNodes()` method takes a list of internal node ids as input and outputs a list of node objects."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {
    "id": "xTl4i5cPWbrh"
   },
   "outputs": [],
   "source": [
    "# If you need to fetch information about node objects based on their internal node ids, you can use gds.util.asNodes\n",
    "pagerank_df['node_object'] =  gds.util.asNodes(pagerank_df['nodeId'].to_list())"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "fa-iw0etaubY"
   },
   "source": [
    "The `node_object` column now contains the referenced node objects. Node objects are defined in the underlying Neo4j Python driver. You can reference the [official documentation if you want to examine all the possible methods of the node object](https://neo4j.com/docs/api/python-driver/current/api.html#node).\n",
    "\n",
    "In this example, we will extract the `name` property from node objects and then visualize a bar chart of the top ten characters with the highest PageRank score."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/",
     "height": 652
    },
    "id": "6drUBiMBWqpJ",
    "outputId": "9e3c906f-7d87-4404-d231-36f6fc0639cf"
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "(array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]),\n",
       " [Text(0, 0, 'Harry Potter'),\n",
       "  Text(1, 0, 'Ronald Weasley'),\n",
       "  Text(2, 0, 'Hermione Granger'),\n",
       "  Text(3, 0, 'Rubeus Hagrid'),\n",
       "  Text(4, 0, 'Severus Snape'),\n",
       "  Text(5, 0, 'Dudley Dursley'),\n",
       "  Text(6, 0, 'Draco Malfoy'),\n",
       "  Text(7, 0, 'Vernon Dursley'),\n",
       "  Text(8, 0, 'Albus Dumbledore'),\n",
       "  Text(9, 0, 'Neville Longbottom')])"
      ]
     },
     "execution_count": 12,
     "metadata": {},
     "output_type": "execute_result"
    },
    {
     "data": {
      "image/png": "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\n",
      "text/plain": [
       "<Figure size 1152x648 with 1 Axes>"
      ]
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    }
   ],
   "source": [
    "# Extract name properties from the node object\n",
    "pagerank_df['name'] = [n['name'] for n in pagerank_df['node_object']]\n",
    "# Draw a bar chart\n",
    "plt.figure(figsize=(16,9))\n",
    "sns.barplot(x='name', y='score', data=pagerank_df.sort_values(by='score', ascending=False).head(10))\n",
    "plt.xticks(rotation=45)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "7e7Jauk_a66I"
   },
   "source": [
    "An additional benefit of having the graph algorithm output available in the Pandas Dataframe is that if you are not experienced with Cypher aggregations, you can simply skip them and do your aggregations in Pandas.\n",
    "\n",
    "As opposed to the `stream` mode of algorithms, the `stats`, `mutate`, and `write` modes do not produce a stream of results. Therefore, the results of Python client methods are not Pandas DataFrame. Instead, those methods output the algorithm metadata in Pandas Series format.\n",
    "\n",
    "For example, let's say we want to execute the mutate mode of the Louvain algorithm."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/"
    },
    "id": "mz5ARGwjEOPV",
    "outputId": "a01ee5f2-da08-40b8-cdaa-e0f3ffdf8ed8"
   },
   "outputs": [
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "c91387e23a5e4b8caa670dd9c10373dd",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "Louvain:   0%|          | 0/100 [00:00<?, ?%/s]"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "mutateMillis                                                             1\n",
      "nodePropertiesWritten                                                  119\n",
      "modularity                                                        0.176974\n",
      "modularities                    [0.15405649883268652, 0.17697414179849236]\n",
      "ranLevels                                                                2\n",
      "communityCount                                                          10\n",
      "communityDistribution    {'p99': 42, 'min': 2, 'max': 42, 'mean': 11.9,...\n",
      "postProcessingMillis                                                    28\n",
      "preProcessingMillis                                                      0\n",
      "computeMillis                                                          779\n",
      "configuration            {'maxIterations': 10, 'seedProperty': None, 'c...\n",
      "Name: 0, dtype: object\n"
     ]
    }
   ],
   "source": [
    "# Louvain mutate\n",
    "louvain_metadata = gds.louvain.mutate(G, mutateProperty='communityId', relationshipWeightProperty='weight')\n",
    "print(louvain_metadata)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "78lP-jZobHYL"
   },
   "source": [
    "The mapping from Cypher to Python is identical as in the PageRank example. The Graph object replaces the name of the projected graph, and additional algorithm configuration parameters are specified using the keyword arguments.\n",
    "\n",
    "We can observe that the algorithm identified ten communities, while the modularity score is relatively low.\n",
    "\n",
    "The `mutate` mode of the algorithm stores the results in the projected in-memory graph. One of the Graph object methods is the `node_properties()` method, which can be used to inspect which node properties are present in the in-memory graph."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/"
    },
    "id": "yGXIi130-3JB",
    "outputId": "dee72534-51c0-4493-98aa-b473b3dfa7ae"
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "['communityId']"
      ]
     },
     "execution_count": 14,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "G.node_properties('Character')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "DeL4V9KHbQga"
   },
   "source": [
    "The result of the `node_properties()` method verifies that the communityId node property was added to the projected graph. \n",
    "\n",
    "Sometimes we want to retrieve the node properties from the projected in-memory graph. Luckily, there is a `gds.graph.streamNodeProperty()` method that fetches node properties from the projected in-memory graph and outputs them as a Pandas DataFrame."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/",
     "height": 206
    },
    "id": "LMgy4UhzFKeD",
    "outputId": "db0ebcfc-8348-48a0-d9d3-190f58e3b8f3"
   },
   "outputs": [
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>nodeId</th>\n",
       "      <th>propertyValue</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>0</td>\n",
       "      <td>103</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>1</td>\n",
       "      <td>103</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>2</td>\n",
       "      <td>3</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>3</td>\n",
       "      <td>3</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>4</td>\n",
       "      <td>14</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "   nodeId  propertyValue\n",
       "0       0            103\n",
       "1       1            103\n",
       "2       2              3\n",
       "3       3              3\n",
       "4       4             14"
      ]
     },
     "execution_count": 15,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# If you want to retrieve node properties from the in-memory graph in the form of a Pandas DataFrame\n",
    "louvain_df = gds.graph.streamNodeProperty(G, 'communityId')\n",
    "louvain_df.head()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "HlABauhObZAd"
   },
   "source": [
    "The first parameter of the `gds.graph.streamNodeProperty()` method is the referenced Graph object. As the second parameter, we define which property we want to retrieve from the in-memory graph.\n",
    "\n",
    "Again, we get the internal node ids in the `nodeId` column. We could use the `gds.util.asNodes()` method to fetch the node objects that the internal node ids reference. Unfortunately, the column with the retrieved node properties has a generic name `propertyValue` . In our case, it would make sense to name the column with the results `communityId` . However, we can do that manually if we need to."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/",
     "height": 363
    },
    "id": "ETnKF9pX-aED",
    "outputId": "2a23dc7c-417a-4852-ce67-ee90028fa888"
   },
   "outputs": [
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>communityId</th>\n",
       "      <th>communitySize</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>14</td>\n",
       "      <td>42</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>8</th>\n",
       "      <td>103</td>\n",
       "      <td>18</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>7</th>\n",
       "      <td>99</td>\n",
       "      <td>16</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>5</th>\n",
       "      <td>69</td>\n",
       "      <td>11</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>31</td>\n",
       "      <td>8</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>4</td>\n",
       "      <td>7</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>48</td>\n",
       "      <td>6</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>6</th>\n",
       "      <td>81</td>\n",
       "      <td>5</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>9</th>\n",
       "      <td>117</td>\n",
       "      <td>4</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>3</td>\n",
       "      <td>2</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "   communityId  communitySize\n",
       "2           14             42\n",
       "8          103             18\n",
       "7           99             16\n",
       "5           69             11\n",
       "3           31              8\n",
       "1            4              7\n",
       "4           48              6\n",
       "6           81              5\n",
       "9          117              4\n",
       "0            3              2"
      ]
     },
     "execution_count": 16,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# Rename columns\n",
    "louvain_df.columns = ['nodeId', 'communityId']\n",
    "# You can do all sorts of pandas operations like aggregations\n",
    "louvain_df.groupby(\"communityId\").size().to_frame(\n",
    "    \"communitySize\"\n",
    ").reset_index().sort_values(by=[\"communitySize\"], ascending=False)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "SbYFdS47bolR"
   },
   "source": [
    "Like mentioned before, the added benefit of dealing with Pandas DataFrames as algorithm output is that you can apply all your Python skills to transform or manipulate the results. In this example, we simply grouped the DataFrame by the `communityId` column and count the members of each community.\n",
    "\n",
    "# Helpful methods\n",
    "In the last part of this post, we will go over some of the helpful methods. The first one that comes to mind is listing all of the already projected in-memory graph with the `gds.graph.list()` method."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/",
     "height": 159
    },
    "id": "GO4zQ6DKLCWG",
    "outputId": "b94c92fc-7d02-46c0-84bc-f8263499827e"
   },
   "outputs": [
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>degreeDistribution</th>\n",
       "      <th>graphName</th>\n",
       "      <th>database</th>\n",
       "      <th>memoryUsage</th>\n",
       "      <th>sizeInBytes</th>\n",
       "      <th>nodeCount</th>\n",
       "      <th>relationshipCount</th>\n",
       "      <th>configuration</th>\n",
       "      <th>density</th>\n",
       "      <th>creationTime</th>\n",
       "      <th>modificationTime</th>\n",
       "      <th>schema</th>\n",
       "      <th>schemaWithOrientation</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>{'p99': 41, 'min': 1, 'max': 83, 'mean': 6.823...</td>\n",
       "      <td>hp-graph</td>\n",
       "      <td>neo4j</td>\n",
       "      <td>2350 KiB</td>\n",
       "      <td>2406760</td>\n",
       "      <td>119</td>\n",
       "      <td>812</td>\n",
       "      <td>{'relationshipProjection': {'INTERACTS': {'ori...</td>\n",
       "      <td>0.057827</td>\n",
       "      <td>2023-02-01T12:15:46.030702248+00:00</td>\n",
       "      <td>2023-02-01T12:16:01.522004066+00:00</td>\n",
       "      <td>{'graphProperties': {}, 'relationships': {'INT...</td>\n",
       "      <td>{'graphProperties': {}, 'relationships': {'INT...</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "                                  degreeDistribution graphName database  \\\n",
       "0  {'p99': 41, 'min': 1, 'max': 83, 'mean': 6.823...  hp-graph    neo4j   \n",
       "\n",
       "  memoryUsage  sizeInBytes  nodeCount  relationshipCount  \\\n",
       "0    2350 KiB      2406760        119                812   \n",
       "\n",
       "                                       configuration   density  \\\n",
       "0  {'relationshipProjection': {'INTERACTS': {'ori...  0.057827   \n",
       "\n",
       "                          creationTime                     modificationTime  \\\n",
       "0  2023-02-01T12:15:46.030702248+00:00  2023-02-01T12:16:01.522004066+00:00   \n",
       "\n",
       "                                              schema  \\\n",
       "0  {'graphProperties': {}, 'relationships': {'INT...   \n",
       "\n",
       "                               schemaWithOrientation  \n",
       "0  {'graphProperties': {}, 'relationships': {'INT...  "
      ]
     },
     "execution_count": 17,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "gds.graph.list()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "d92J_oYCbxxx"
   },
   "source": [
    "Sometimes there are already projected in-memory graphs present in the database. If you don't have a reference to the projected graphs in the form of a Graph object, you cannot execute any graph algorithm. To avoid having to drop and recreate projected graphs, you can use the `gds.graph.get()` method."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "metadata": {
    "id": "UML8KJzWTpjN"
   },
   "outputs": [],
   "source": [
    "# G = gds.graph.get(\"graph name\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "2hIU-lQ2b5xz"
   },
   "source": [
    "When using the shortest path algorithms, you need to provide source and target nodes ids. You could use Cypher statements or you could use the `gds.find_node_id()` method.\n",
    "\n",
    "![find_node.drawio (1).png]()\n",
    "\n",
    "The `gds.find_node_id()` takes in two arguments. The first argument defines the node label we are searching for. In our example, we are searching for the `Character` node label. The second parameter specifies the node properties used to identify the particular node. The node properties are defined as a dictionary or map of key-value pairs, similar to the inline `MATCH` clause. The only difference is that we must add quotes around the key values of properties since otherwise, we would get a NameError in Python."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/"
    },
    "id": "2nTv7PcrTl7B",
    "outputId": "a3dcfc07-b511-4701-fdfe-566f1b41ac31"
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "4"
      ]
     },
     "execution_count": 19,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "gds.find_node_id([\"Character\"], {\"name\":\"Harry Potter\"})"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "zj3XAI8McJdK"
   },
   "source": [
    "The last useful method I will present here is the `drop()` method of a Graph object. It is used to release the projected graph from memory."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 20,
   "metadata": {
    "id": "V23wjoyfKtag"
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "graphName                                                         hp-graph\n",
       "database                                                             neo4j\n",
       "memoryUsage                                                               \n",
       "sizeInBytes                                                             -1\n",
       "nodeCount                                                              119\n",
       "relationshipCount                                                      812\n",
       "configuration            {'relationshipProjection': {'INTERACTS': {'ori...\n",
       "density                                                           0.057827\n",
       "creationTime                           2023-02-01T12:15:46.030702248+00:00\n",
       "modificationTime                       2023-02-01T12:16:01.522004066+00:00\n",
       "schema                   {'graphProperties': {}, 'relationships': {'INT...\n",
       "schemaWithOrientation    {'graphProperties': {}, 'relationships': {'INT...\n",
       "Name: 0, dtype: object"
      ]
     },
     "execution_count": 20,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# Drop a projected in-memory graph\n",
    "G.drop()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "n0u3CxSccPgi"
   },
   "source": [
    "# Conclusion\n",
    "The Neo4j Graph Data Science Python client is designed to help you integrate Neo4j and its graph algorithms into your Python analytical workflows. The syntax of the Python client mimics the GDS Cypher procedures. Since not all graph algorithms are documented to be used as Python client method, you need to take into account the following guidelines when translating a Cypher procedure to a Python client method:\n",
    "* When specifying a map or a dictionary as a parameter to any method, make sure to add quotes around the keys\n",
    "* Instead of referencing the projected graph by its name, you need to input the * Graph object as the first parameter of graph algorithms\n",
    "* Algorithm specific configuration parameter can be specified using keyword arguments\n",
    "* The stream mode of graph algorithms outputs a Pandas DataFrame\n",
    "* Other algorithm modes like stats , write , and mutate output the metadata of the algorithm call as a Pandas Series\n",
    "\n",
    "I am very excited about the new Python client and will be definitely using it in my workflows. Try it out and if you have any feedback please report it to the [official GitHub repository of the Python client](https://github.com/neo4j/graph-data-science-client)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 21,
   "metadata": {
    "id": "8-VFIcGAcYQg"
   },
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "colab": {
   "authorship_tag": "ABX9TyMLODtjsTX2gWhXe5ADDUdP",
   "include_colab_link": true,
   "name": "gds-python.ipynb",
   "provenance": []
  },
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.8.8"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 1
}
